﻿using System;
using System.Collections.Generic;
using System.Text;

using Microsoft.Scripting;

namespace DlrBasic.Parser
{
    /// <summary>
    /// Simple BASIC tokenizer.
    /// </summary>
    class DlrBasicTokenizer 
    {
        SourceUnit source;
        string text;
        Token currentToken;
        IEnumerator<Token> tokenEnumerator;

        internal SourceUnit Source
        {
            get { return source; }
        }

        internal Token CurrentToken
        {
            get { return currentToken; }
        }

        public DlrBasicTokenizer(SourceUnit source)
        {
            this.source = source;
            this.text = source.GetCode();
            this.tokenEnumerator = GetTokens();
        }

        internal bool IsKeyword(int keywordId)
        {
            return CurrentToken.Kind == TokenKind.Keyword &&
                CurrentToken.ItemId == keywordId;
        }

        internal bool IsKeyword(params int[] keywordIds)
        {
            if (CurrentToken.Kind == TokenKind.Keyword)
            {
                for (int i = 0; i < keywordIds.Length; i++)
                {
                    if (CurrentToken.ItemId == keywordIds[i]) return true;
                }
            }
            return false;
        }

        internal bool IsOperator(int operatorId)
        {
            return CurrentToken.Kind == TokenKind.Operator &&
                CurrentToken.ItemId == operatorId;
        }

        internal bool IsOperator(params int[] operatorIds)
        {
            if (CurrentToken.Kind == TokenKind.Operator)
            {
                for (int i = 0; i < operatorIds.Length; i++)
                {
                    if (CurrentToken.ItemId == operatorIds[i]) return true;
                }
            }
            return false;
        }

        public Token NextToken()
        {
            if (tokenEnumerator.MoveNext())
                currentToken = tokenEnumerator.Current;
            else
            {
                SourceLocation end = currentToken.Span.End;
                currentToken = new Token(new SourceSpan(end, end), TokenKind.EndOfFile, null);
            }
            return currentToken;
        }

        public void SkipToNewLine()
        {
            while (CurrentToken != null && CurrentToken.Kind != TokenKind.NewLine)
            {
                NextToken();
            }
        }

        private IEnumerator<Token> GetTokens()
        {
            int index = 0;
            int line = 1;
            int column = 1;

            while (index < text.Length)
            {
                if (Char.IsWhiteSpace(text, index))
                {
                    int whiteSpaceChordLength;
                    bool isNewLine;
                    ParseWhitespaceChords(text, index, out isNewLine, out whiteSpaceChordLength);
                    if (isNewLine)
                    {
                        yield return new Token(new SourceSpan(
                            new SourceLocation(index, line, column), new SourceLocation(index + whiteSpaceChordLength, line, column + whiteSpaceChordLength)),
                            TokenKind.NewLine, text.Substring(index, whiteSpaceChordLength));
                        line++;
                        index += whiteSpaceChordLength;
                        column = 1;
                    }
                    else
                    {
                        column += whiteSpaceChordLength;
                        index += whiteSpaceChordLength;
                    }
                    continue;
                }

                SourceLocation startLocation = Source.MakeLocation(index, line, column);

                if(IsNumberStart(text, index))
                {
                    StringBuilder sb = new StringBuilder();
                    //if (text[index] == '-' || text[index] == '+')
                    //{
                    //    sb.Append(text[index++]);
                    //}                    
                    while (index < text.Length && Char.IsNumber(text, index))
                    {
                        sb.Append(text[index++]);
                    }
                    if (index < text.Length && text[index] == '.')
                    {
                        sb.Append(text[index++]);
                        while (index < text.Length && Char.IsNumber(text, index))
                        {
                            sb.Append(text[index++]);
                        }
                    }
                    if (index + 1 < text.Length && IsExponentialFormSign(text[index])
                        && IsIntegerNumberStart(text, index + 1))
                    {
                        sb.Append(text[index++]);
                        sb.Append(text[index++]); // skip +,-, or first digit
                        while (index < text.Length && Char.IsNumber(text, index))
                        {
                            sb.Append(text[index++]);
                        }                        
                    }
                    //else if(index < text.Length && text[index] == '!' && text[index] == '#')
                    //{
                    //        sb.Append(text[index++]);
                    //}
                    
                    column += sb.Length;
                    SourceLocation endLocation = new SourceLocation(index, line, column);
                    yield return new Token(new SourceSpan(startLocation, endLocation),
                         TokenKind.NumericConstant, sb.ToString());
                }
                else if (IsSpecialNumberStart(text, index))
                {
                    StringBuilder sb = new StringBuilder();
                    sb.Append(text[index++]);
                    char ch = Char.ToUpperInvariant(text[index]);
                    if (ch == 'H')
                    {
                        sb.Append(text[index++]);
                        ch = Char.ToUpperInvariant(text[index]);
                        while (Char.IsNumber(ch) || (ch >= 'A' && ch <= 'F'))
                        {
                            sb.Append(text[index++]);
                            ch = Char.ToUpperInvariant(text[index]);
                        }
                    }
                    else
                    {
                        if (ch == 'O') sb.Append(text[index]);

                        ch = text[index];
                        while (ch >= '0' && ch <= '7')
                        {
                            sb.Append(text[index++]);
                            ch = text[index];
                        }
                    }
                    column += sb.Length;
                    SourceLocation endLocation = new SourceLocation(index, line, column);
                    yield return new Token(new SourceSpan(startLocation, endLocation),
                         TokenKind.BasedInteger, sb.ToString());
                }
                else if (text[index] == '\"')
                {
                    StringBuilder sb = new StringBuilder();
                    sb.Append(text[index++]);
                    while (index < text.Length && 
                        (text[index] != '\"' && text[index] != '\r' && text[index] != '\n') )
                    {
                        sb.Append(text[index++]);
                    }
                    if(index < text.Length && text[index] == '\"')
                        sb.Append(text[index++]);

                    column += sb.Length;
                    SourceLocation endLocation = new SourceLocation(index, line, column);
                    yield return new Token(new SourceSpan(startLocation, endLocation),
                         TokenKind.StringConstant, sb.ToString());
                }
                else if (Char.IsLetter(text, index))
                {
                    // Identifier or keyword
                    int startIndex = index;
                    bool keywordCheck = true;
                    while (index < text.Length &&
                        (Char.IsLetterOrDigit(text, index) || text[index] == '.'))
                    {
                        if (keywordCheck && !Char.IsLetter(text, index))
                        {
                            keywordCheck = true;

                            if (IsBasicKeyword(text.Substring(startIndex, index - startIndex))) break;
                        }
                        index++;
                    }
                    column += index - startIndex;
                    string name = text.Substring(startIndex, index - startIndex);
                    bool isKeyword = IsBasicKeyword(text.Substring(startIndex, index - startIndex));
                    SourceLocation endLocation = new SourceLocation(index, line, column);

                    string indentifier = text.Substring(startIndex, index - startIndex);
                    int itemId = 0;
                    if (isKeyword)
                    {
                        itemId = Keywords.GetKeywordId(indentifier);
                    }

                    yield return new Token(new SourceSpan(startLocation, endLocation),
                        isKeyword ? TokenKind.Keyword : TokenKind.Identifier,
                        indentifier, itemId);
                }
                else
                {
                    int startIndex = index;
                    TokenKind kind = TokenKind.Error;
                    int itemId = 0;
                    switch (text[index++])
                    {
                        case '&':
                        case '?':
                            kind = TokenKind.Keyword;
                            itemId = Keywords.PrintKeywordId;
                            break;
                        case '\'':
                            kind = TokenKind.ShortComment;
                            break;
                        case ':':
                            kind = TokenKind.NewStatement;
                            break;
                        case ';':
                            kind = TokenKind.Semicolon;
                            break;
                        case ',':
                            kind = TokenKind.Colon;
                            break;
                        case '!':
                            kind = TokenKind.TypeSignature;
                            itemId = Operators.SingleSignatureId;
                            break;
                        case '$':
                            kind = TokenKind.TypeSignature;
                            itemId = Operators.StringSignatureId;
                            break;
                        case '#':
                            kind = TokenKind.TypeSignature;
                            itemId = Operators.DoubleSignatureId;
                            break;
                        case '%':
                            kind = TokenKind.TypeSignature;
                            itemId = Operators.IntSignatureId;
                            break;
                        case '^':
                            kind = TokenKind.Operator;
                            itemId = Operators.PowerId;
                            break;
                        case '-':
                            kind = TokenKind.Operator;
                            itemId = Operators.SubstractId;
                            break;
                        case '/':
                            kind = TokenKind.Operator;
                            itemId = Operators.DivideId;
                            break;
                        case '\\':
                            kind = TokenKind.Operator;
                            itemId = Operators.IntDivideId;
                            break;
                        case '+':
                            kind = TokenKind.Operator;
                            itemId = Operators.AddId;
                            break;
                        case '=':
                            if (index < text.Length && text[index] == '<')
                            {
                                itemId = Operators.LessThenOrEqualId;
                                ++index;
                            }
                            else if (index < text.Length && text[index] == '>')
                            {
                                itemId = Operators.GreaterThenOrEqualId;
                                ++index;
                            }
                            else
                                itemId = Operators.EqualId;

                            kind = TokenKind.Operator;
                            break;
                        case '*':
                            if (index < text.Length && text[index] == '*')
                            {
                                ++index;
                                itemId = Operators.PowerId;
                            }
                            else
                                itemId = Operators.MultiplyId;
                            kind = TokenKind.Operator;
                            break;
                        case '(':
                        case '[':
                            kind = TokenKind.LeftParenthesis;
                            break;
                        case ')':
                        case ']':
                            kind = TokenKind.RightParenthesis;
                            break;
                        case '<':
                            if (index < text.Length && text[index] == '=')
                            {
                                itemId = Operators.LessThenOrEqualId;
                                ++index;
                            }
                            else if (index < text.Length && text[index] == '>')
                            {
                                itemId = Operators.NotEqualId;
                                ++index;
                            }
                            else
                                itemId = Operators.LessThenId;
                            kind = TokenKind.Operator;
                            break;
                        case '>':
                            if (index < text.Length && text[index] == '=')
                            {
                                itemId = Operators.GreaterThenOrEqualId;
                                ++index;
                            }
                            else if (index < text.Length && text[index] == '<')
                            {
                                itemId = Operators.NotEqualId;
                                ++index;
                            }
                            else
                                itemId = Operators.GreaterThenId;
                            kind = TokenKind.Operator;
                            break;
                    }
                    column += index - startIndex;
                    SourceLocation endLocation = new SourceLocation(index, line, column);
                    yield return new Token(new SourceSpan(startLocation, endLocation),
                         kind, text.Substring(startIndex, index - startIndex), itemId);
                }
            }
        }

        static void ParseWhitespaceChords(string text, int index, out bool isNewLine, out int length)
        {
            length = 1;
            isNewLine = false;
            if (text[index] == '\n' || text[index] == '\r')
            {
                if (index + 1 < text.Length)
                {
                    if ((text[index + 1] == '\n' || text[index + 1] == '\r') &&
                        text[index] != text[index + 1])
                    {
                        length = 2;
                        if (index + 2 < text.Length &&
                            Char.IsWhiteSpace(text, index + 2) && text[index + 2] != '\n' && text[index + 2] != '\r')
                        {
                            length = 3;
                        }
                        else
                        {
                            isNewLine = true;
                        }
                    }
                    else
                    {
                        if (index + 1 < text.Length &&
                            Char.IsWhiteSpace(text, index + 1) && text[index + 1] != '\n' && text[index + 1] != '\r')
                        {
                            length = 2;
                        }
                        else
                        {
                            isNewLine = true;
                        }
                    }
                }
                else
                {
                    isNewLine = true;
                }
            }
        }

        static bool IsBasicKeyword(string name)
        {
            return Keywords.IsKeyword(name);
        }

        static bool IsComment(string text, int index)
        {
            if (text[index] == '\'')
                return true;
            return false;
        }

        static bool IsSpecialNumberStart(string text, int index)
        {
            if (text[index] == '&' && index + 1 < text.Length)
            {
                char ch = Char.ToUpperInvariant(text[index + 1]);
                if (ch == 'H' && index + 2 < text.Length)
                {
                    ch = Char.ToUpperInvariant(text[index + 2]);
                    return Char.IsNumber(ch) || (ch >= 'A' && ch <= 'F');
                }
                else if (ch == 'O' && index + 2 < text.Length) 
                {
                    ch = text[index + 2];
                    return ch >= '0' && ch <= '7';
                }
                else
                    return ch >= '0' && ch <= '7';
            }
            return false;
        }

        static bool IsExponentialFormSign(char ch)
        {
            return ch == 'E' || ch == 'e' || ch == 'D' || ch == 'd';
        }

        static bool IsNumberStart(string text, int index)
        {
            if(Char.IsNumber(text, index))
                return true;
            else if (text[index] == '.')
            {
                if (index + 1 < text.Length && Char.IsNumber(text, index + 1))
                    return true;
            }
            return false;
        }

        static bool IsIntegerNumberStart(string text, int index)
        {
            if (Char.IsNumber(text, index))
                return true;
            else if (text[index] == '-' || text[index] == '+')
            {
                if (index + 1 < text.Length && Char.IsNumber(text, index + 1))
                    return true;
            }
            return false;
        }
    }
}
